// TODO: CODE REVIEW & CLEANUP!
//
// TouchScrollViewerWithFocus.cs
//
// Implements TouchScrollViewerWithFocus and related types.
//

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using HomeUX.ClientControlSupport;
using HomeUX.UiUtilities;
using HomeUX.Utilities;

namespace HomeUX.Controls
{

/// <summary>
/// A touch-friendly control that vertically scrolls a grid of items (such as <r>TouchButton</r>
/// objects) that are capable of pseudofocus.
/// </summary>
///
/// <remarks>
/// <para>
/// Unlike <r>TouchScrollViewer</r>, which treats its extent (i.e. the content it scrolls) as a
/// single uniform object, or <r>TouchScrollViewerWithRows</r>, whose extent contains a series of
/// "rows" that are arbitrary elemenents, the extent of <r>TouchScrollViewerWithFocus</r> contains
/// <b>rows</b> which must each contain one or more <b>items</b>.  Each item is an arbitrary
/// element, except it must implement <r>IPseudofocusable</r>, since a key distinguishing feature
/// of <r>TouchScrollViewerWithFocus</r> is that it displays one item at a time as having
/// pseudofocus.  (It's called "pseudofocus" because the actual focus is on the
/// <r>TouchScrollViewerWithFocus</r> itself -- or, more precisely, a hidden control within it
/// that holds onto focus.  In fact, items must have <n>IsTabStop</n> equal to <n>false</n>, since
/// we don't want an item getting real focus.)
/// </para>
/// <para>
/// Scrolling can be accomplished in several ways: on-screen scroll buttons, a slider (which works
/// like a scrollbar), keyboard keys (see below), and dragging the extent directly.
/// </para>
/// <para>
/// Since <n>TouchScrollViewerWithFocus</n> maintains pseudofocus, keyboard navigation (using e.g.
/// arrow keys, Page Up, Home, etc.) perform differently than on-screen scroll buttons (e.g. the
/// line up or page up button): the former move the pseudofocus (ensuring that the pseudofocused
/// item is always in view); the latter leave the pseudofocus alone and simply scrolls the extent
/// within the viewport (i.e. the same behavior as <r>TouchScrollViewerWithRows</r>).  Dragging
/// the extent also does not later pseudofocus.
/// </para>
/// <para>
/// Note: Unless you override <ar>Rows</ar>, <n>Content</n> must conform to some specific rules --
/// see <ar>Rows</ar> for more information.  Also, unless you override <r>GetRowItems</r>, each row
/// must conform to some specific rules -- see <r>GetRowItems</r> for more information.
/// </para>
/// <para>
/// If an item implements <r>IPerformKeyEvent</r>, then keyboard keys (other than navigation keys)
/// are sent to the item with pseudofocus.
/// </para>
/// </remarks>
/// 
public class TouchScrollViewerWithFocus : TouchScrollViewerWithRows
{
    public static readonly DependencyProperty IgnoreItemsPerRowProperty =
        DependencyProperty.Register("IgnoreItemsPerRow", typeof(int),
        typeof(TouchScrollViewerWithFocus), null);

    /// <summary>
    /// <n>true</n> if this control has focus.
    /// </summary>
    bool _hasFocus;

    /// <summary>
    /// The item that currently has focus, or <n>null</n> if no item has focus.
    /// </summary>
    IPseudofocusable _pseudofocusedItem;

    /// <summary>
    /// When a key press (e.g. Space) is sent to an item, <r>_itemKeyEventArgs</r> is set to
    /// the <n>KeyEventArgs</n> of that key press; when the key is released,
    /// <r>_itemKeyEventArgs</r> is set to <n>null</n>.
    /// </summary>
    KeyEventArgs _itemKeyEventArgs = null;

    public TouchScrollViewerWithFocus()
    {
        DefaultStyleKey = typeof(TouchScrollViewer);
    }

    public int IgnoreItemsPerRow
    {
        get
        {
            return (int)GetValue(IgnoreItemsPerRowProperty);
        }
        set
        {
            SetValue(IgnoreItemsPerRowProperty, value);
        }
    }

    /// <summary>
    /// Returns the first item in the first row, or <n>null</n> if there are no rows.
    /// </summary>
    ///
    public IPseudofocusable FirstItem
    {
        get
        {
            IList<FrameworkElement> rows = Rows;
			if (rows.Count == 0)
				return null;
            FrameworkElement row = rows[0];
            IList<IPseudofocusable> items = GetRowItems(row);
            return items[0];
        }
    }

    /// <summary>
    /// Returns the last item in the last row, or <n>null</n> if there are no rows.
    /// </summary>
    ///
    public IPseudofocusable LastItem
    {
        get
        {
            IList<FrameworkElement> rows = Rows;
			if (rows.Count == 0)
				return null;
            FrameworkElement row = rows[rows.Count - 1];
            IList<IPseudofocusable> items = GetRowItems(row);
            return items[items.Count - 1];
        }
    }

    /// <summary>
    /// Gets or sets the pseudofocused item, or <n>null</n> if there is (or should be) no
    /// pseudofocused item.
    /// </summary>
    ///
    public IPseudofocusable PseudofocusedItem
    {
        get
        {
            return _pseudofocusedItem;
        }
        set
        {
            if (!TrySetPseudofocus(value))
                throw new ArgumentException("Item isn't within this container", "value");
        }
    }

    /// <summary>
    /// Attempts to set pseudofocus on a given item.
    /// </summary>
    ///
    /// <param name="item">The item to set pseudofocus to.</param>
    ///
    /// <returns>
    /// <n>true</n> if pseudofocus was set successfully, <n>false</n> if not.
    /// </returns>
    ///
    public bool TrySetPseudofocus(IPseudofocusable item)
    {
        if ((item != null) && !ContainsItem(item))
            return false;
        SetPseudofocusedItem(item, ScrollAlignment.IntoView);
        return true;
    }

    /// <summary>
    /// Returns <n>true</n> if a given object is an item row in this
    /// <r>TouchScrollViewerWithFocus</r>, <n>false</n> if not.  This operation is O(1).
    /// </summary>
    ///
    /// <param name="row">The row to check.</param>
    ///
    bool ContainsItem(IPseudofocusable item)
    {
        FrameworkElement row = GetRowOfItem(item);
        if (row == null)
            return false;
        return ContainsRow(row);
    }

    /// <summary>
    /// Returns the items within a given row.
    /// </summary>
    ///
    /// <param name="row">The row to get the item for.  This is one of the values in<ar>Rows</ar>.
    ///     </param>
    /// 
    /// <remarks>
    /// <para>
    /// The default implementation assumes that each row consists of a <n>Panel</n> (e.g. a
    /// horizontal <n>StackPanel</n>) whose only children are items which can receive
    /// pseudofocus (i.e. which implement <r>IPseudofocusable</r>).
    /// </para>
    /// </remarks>
    ///
    protected virtual IList<IPseudofocusable> GetRowItems(FrameworkElement row)
    {
        UIElementCollection items = ((Panel)row).Children;
        int skip = IgnoreItemsPerRow;
        return new SimpleList<IPseudofocusable>(items.Count - skip,
            i => (IPseudofocusable)items[skip + i]);
    }

    protected virtual FrameworkElement GetRowOfItem(IPseudofocusable item)
    {
        return (FrameworkElement)((FrameworkElement)item).Parent;
    }

    /// <summary>
    /// Performs the action of a specified keyboard navigation button (i.e. an arrow key, Page Up,
    /// Page Down, Home, End).
    /// </summary>
    ///
    /// <param name="key">The key.</param>
    ///     
    /// <returns>
    /// <n>true</n> if the key was handled, <n>false</n> if not.
    /// </returns>
    ///
    protected override bool PerformKeyboardNavigation(Key key)
    {
		// return false if <key> isn't a keyboard navigation key
        switch (key)
        {
        case Key.Up:
        case Key.Down:
        case Key.Left:
        case Key.Right:
        case Key.PageUp:
        case Key.PageDown:
        case Key.Home:
        case Key.End:
			break;
		default:
			return false;
        }

        // nothing to do if no item has pseudofocus (which should only happen if there are no
        // items); note that we return true here because we don't want different behavior for
		// keyboard navigation keys when there is no pseudoficus
        if (_pseudofocusedItem == null)
            return true;

        // set <newItem> to the item that should have focus after we perform the navigation
        // specified by <key>; if the focus shouldn't change (e.g. <key> is Key.Up and we're
        // already at the top row), just return
        IPseudofocusable newItem = FindItemGivenKeyboardKey(_pseudofocusedItem, key);
        if (newItem == null)
            return true;

        // scroll <newItem> into view, aligning appropriately within the viewport
        ScrollAlignment alignment;
        if (key == Key.PageUp)
            alignment = ScrollAlignment.Top;
        else
        if (key == Key.PageDown)
            alignment = ScrollAlignment.Bottom;
        else
            alignment = ScrollAlignment.IntoView;
        SetPseudofocusedItem(newItem, alignment);
        return true;
    }

    protected override void OnKeyDown(KeyEventArgs e)
    {
        // give the base class a chance to handle the event; if it did, return
        base.OnKeyDown(e);
        if (e.Handled)
            return;

        // send the key to the item (if the item implements IPerformKeyEvent); but, if the user
        // pressed a second key that needs to go to the item while a first key is still pressed,
        // simulate releasing the first key
        if ((_itemKeyEventArgs != null) && (_itemKeyEventArgs.Key != e.Key))
        {
            _itemKeyEventArgs.Handled = false;
            PressItemButton(false, _itemKeyEventArgs);
        }

        // simulate pressing the key specified by <e>
        PressItemButton(true, e);
    }

    protected override void OnKeyUp(KeyEventArgs e)
    {
        // give the base class a chance to handle the event; if it did, return
        base.OnKeyUp(e);
        if (e.Handled)
            return;

        // if we sent a key-down event to the pseudofocused item (if any), send a key-up event now
        if ((_itemKeyEventArgs != null) && (_itemKeyEventArgs.Key == e.Key))
            PressItemButton(false, e);
    }

    protected override void OnGotFocus(RoutedEventArgs e)
    {
        base.OnGotFocus(e);

        // keep track of whether this control has focus
        _hasFocus = true;

        // show pseudofocus
        if (_pseudofocusedItem != null)
            _pseudofocusedItem.IsPseudofocused = true;
    }

    protected override void OnLostFocus(RoutedEventArgs e)
    {
        base.OnLostFocus(e);

        // keep track of whether this control has focus
        _hasFocus = false;

        if (_itemKeyEventArgs != null)
        {
            _itemKeyEventArgs.Handled = false;
            PressItemButton(false, _itemKeyEventArgs);
            _itemKeyEventArgs = null;
        }

        // hide pseudofocus
        if (_pseudofocusedItem != null)
            _pseudofocusedItem.IsPseudofocused = false;
    }

    void PressItemButton(bool press, KeyEventArgs e)
    {
        var performKeyEvent = _pseudofocusedItem as IPerformKeyEvent;
        if (performKeyEvent != null)
        {
            if (press)
            {
                performKeyEvent.PerformKeyDown(e);
                _itemKeyEventArgs = e;
            }
            else
            {
                performKeyEvent.PerformKeyUp(e);
                _itemKeyEventArgs = null;
            }
        }
    }

    /// <summary>
    /// Given an extent item and a <n>Key</n> (e.g. <n>Key.Up</n> to navigate one row up), the
    /// item which would be navigated to is returned (or <n>null</n> if none).
    /// </summary>
    ///
    /// <param name="initialItem">The starting item.</param>
    ///
    /// <param name="key">The navigation key.</param>
    ///
    IPseudofocusable FindItemGivenKeyboardKey(IPseudofocusable initialItem, Key key)
    {
        FrameworkElement row = GetRowOfItem(initialItem);
        IList<IPseudofocusable> items = GetRowItems(row);
        int itemIndex = items.IndexOf(initialItem);

        // if left arrow was pressed and there's an item to the left of the current item in this
        // row, return it
        if ((key == Key.Left) && (itemIndex > 0))
            return items[itemIndex - 1];

        // if right arrow was pressed and there's an item to the right of the current item in this
        // row, return it
        if ((key == Key.Right) && (itemIndex < items.Count - 1))
            return items[itemIndex + 1];

        IList<FrameworkElement> rows = Rows;
        int rowIndex = GetIndexOfRow(row);

        if ((key == Key.Left) || (key == Key.Up))
        {
            if (rowIndex > 0)
            {
                row = rows[rowIndex - 1];
                items = GetRowItems(row);
                if (key == Key.Left)
                    itemIndex = items.Count - 1;
                return items[Math.Max(itemIndex, 0)];
            }
            else
                return null; // at top of list
        }

        if ((key == Key.Right) || (key == Key.Down))
        {
            if (rowIndex < rows.Count - 1)
            {
                row = rows[rowIndex + 1];
                items = GetRowItems(row);
                if (key == Key.Right)
                    itemIndex = 0;
                return items[Math.Min(itemIndex, items.Count - 1)];
            }
            else
                return null; // at bottom of list
        }

        if (key == Key.PageDown)
        {
            // return the first item <r> below the initial item <q> such that if <r> were
            // scrolled into view using ScrollAlignment.Bottom then <q> would be at least partially
            // hidden from view; in other words, find <r> such that the distance between r.Bottom +
            // ScrollPeekAmount and q.Top exceeds the height of the viewport
            IPseudofocusable r = initialItem;
            double qTop = double.NaN;
            while (true)
            {
                row = GetRowOfItem(r);
                Rect rect = UiUtil.GetElementBoundingRect(row, Extent);
                if (Double.IsNaN(qTop))
                    qTop = rect.Top;
                if (rect.Bottom + ScrollPeekAmount - qTop > ScrollViewer.ViewportHeight)
                {
                    if (r == initialItem) // e.g. item is as tall as viewport
                        return FindItemGivenKeyboardKey(r, Key.Down);
                    break;
                }
                IPseudofocusable rNext = FindItemGivenKeyboardKey(r, Key.Down);
                if (rNext == null)
                    break;
                r = rNext;
            }
            return (r == initialItem) ? null : r;
        }

        if (key == Key.PageUp)
        {
            // return the first item <r> above the initial item <q> such that if <r> were
            // scrolled into view using ScrollAlignment.Top then <q> would be at least partially
            // hidden from view; in other words, find <r> such that the distance between r.Top -
            // ScrollPeekAmount and q.Bottom exceeds the height of the viewport
            IPseudofocusable r = initialItem;
            double qBottom = double.NaN;
            while (true)
            {
                row = GetRowOfItem(r);
                Rect rect = UiUtil.GetElementBoundingRect(row, Extent);
                if (Double.IsNaN(qBottom))
                    qBottom = rect.Bottom;
                if (qBottom - (rect.Top - ScrollPeekAmount) > ScrollViewer.ViewportHeight)
                {
                    if (r == initialItem) // e.g. item is as tall as viewport
                        return FindItemGivenKeyboardKey(r, Key.Up);
                    break;
                }
                IPseudofocusable rNext = FindItemGivenKeyboardKey(r, Key.Up);
                if (rNext == null)
                    break;
                r = rNext;
            }
            return (r == initialItem) ? null : r;
        }

        if (key == Key.Home)
            return FirstItem;

        if (key == Key.End)
            return LastItem;

        return null;
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        Dispatcher.BeginInvoke(delegate
        {
            SetInitialPseudofocus();
        });
    }

    protected virtual void SetInitialPseudofocus()
    {
        SetPseudofocusedItem(FirstItem, ScrollAlignment.None);
    }

    void SetPseudofocusedItem(IPseudofocusable item, ScrollAlignment alignment)
    {
        if (_pseudofocusedItem != null)
            _pseudofocusedItem.IsPseudofocused = false;
        _pseudofocusedItem = item;
        if (_pseudofocusedItem != null)
        {
            if (_hasFocus)
                _pseudofocusedItem.IsPseudofocused = true;
            if (alignment != ScrollAlignment.None)
            {
                if (!ScrollIntoView(_pseudofocusedItem, alignment))
                {
                    // if scrolling failed (possibly because it was too early in the layout
                    // process), try one more time asynchronously
                    Dispatcher.BeginInvoke(delegate
                    {
                        if (_pseudofocusedItem != null)
                            ScrollIntoView(_pseudofocusedItem, alignment);
                    });
                }
            }
        }
    }

    bool ScrollIntoView(IPseudofocusable item, ScrollAlignment alignment)
    {
        // set <row> to the row containing <item>
        FrameworkElement row = GetRowOfItem(item);

        // scroll <row> into view
        return ScrollIntoView(row, alignment);
    }

}

}

